import _ from "lodash";
import React, { useCallback, useMemo, useRef } from "react";
import styled from "styled-components";
import { AreaClosed, Bar, Line, LinePath } from "@visx/shape";
import { curveMonotoneX } from "@visx/curve";
import { scaleLinear, scaleTime } from "@visx/scale";
import { AxisBottom, AxisLeft } from "@visx/axis";

import { defaultStyles, TooltipWithBounds, useTooltip } from "@visx/tooltip";

import { GridColumns, GridRows } from "@visx/grid";

import { localPoint } from "@visx/event";
import { LinearGradient } from "@visx/gradient";
import { bisector, extent, max } from "d3-array";
import { timeFormat } from "d3-time-format";
import { NormalizedMetricsData } from "../../../cluster-dashboard/expanded-chart/metrics/types";
import { AggregatedDataColors } from "../../../cluster-dashboard/expanded-chart/metrics/utils";
import { ColorTheme } from "./utils";

var globalData: NormalizedMetricsData[];

// util
const formatDate = timeFormat("%H:%M:%S %b %d, '%y");

const hourFormat = timeFormat("%H:%M");
const dayFormat = timeFormat("%b %d");

// map resolutions to formats
const formats: { [range: string]: (date: Date) => string } = {
    "1H": hourFormat,
    "6H": hourFormat,
    "1D": hourFormat,
    "1M": dayFormat,
};

// accessors
const getDate = (d: NormalizedMetricsData) => new Date(d.date * 1000);
const getValue = (d: NormalizedMetricsData) =>
    d?.value && Number(d.value?.toFixed(4));

const bisectDate = bisector<NormalizedMetricsData, Date>(
    (d) => new Date(d.date * 1000)
).left;

export type AreaProps = {
    data: NormalizedMetricsData[];
    aggregatedData?: Record<string, NormalizedMetricsData[]>;
    dataKey: string;
    isAggregated?: boolean;
    hpaEnabled?: boolean;
    hpaData?: NormalizedMetricsData[];
    resolution: string;
    width: number;
    height: number;
    margin?: { top: number; right: number; bottom: number; left: number };
};

const AreaChart: React.FunctionComponent<AreaProps> = ({
    data,
    aggregatedData = {},
    dataKey,
    isAggregated = false,
    hpaEnabled = false,
    hpaData = [],
    resolution,
    width,
    height,
    margin = { top: 0, right: 0, bottom: 0, left: 0 },
}) => {
    globalData = data;

    const {
        showTooltip,
        hideTooltip,
        tooltipData,
        tooltipTop,
        tooltipLeft,
    } = useTooltip<{
        data: NormalizedMetricsData;
        tooltipHpaData: NormalizedMetricsData;
        aggregatedData?: Record<string, NormalizedMetricsData>;
    }>();

    const svgContainer = useRef();
    // bounds
    const innerWidth = width - margin.left - margin.right - 40;
    const innerHeight = height - margin.top - margin.bottom - 20;
    const isHpaEnabled = hpaEnabled && !!hpaData.length;

    // scales
    const dateScale = useMemo(
        () =>
            scaleTime({
                range: [margin.left, innerWidth + margin.left],
                domain: extent(
                    [...globalData, ...(isHpaEnabled ? hpaData : [])],
                    getDate
                ) as [Date, Date],
            }),
        [margin.left, width, height, data, hpaData, isHpaEnabled]
    );
    const valueScale = useMemo(
        () =>
            scaleLinear({
                range: [innerHeight + margin.top, margin.top],
                domain: [
                    0,
                    1.25 *
                    max(
                        [
                            ...globalData,
                            ...Object.values(aggregatedData).flat(),
                            ...(isHpaEnabled ? hpaData : []),
                        ],
                        getValue
                    ),
                ],
                nice: true,
            }),
        [margin.top, width, height, data, hpaData, isHpaEnabled]
    );

    const getAggregatedDataTooltip = (x0: Date) => {
        let aggregatedTooltipData: Record<string, NormalizedMetricsData> = {};
        for (let [key, values] of Object.entries(aggregatedData)) {
            const index = bisectDate(values, x0, 1);
            const d0 = values[index - 1];
            const d1 = values[index];
            let d = d0;

            if (d1 && getDate(d1)) {
                d =
                    x0.valueOf() - getDate(d0).valueOf() >
                        getDate(d1).valueOf() - x0.valueOf()
                        ? d1
                        : d0;
            }

            aggregatedTooltipData[key] = d;
        }

        return aggregatedTooltipData;
    };

    // tooltip handler
    const handleTooltip = useCallback(
        (
            event: React.TouchEvent<SVGRectElement> | React.MouseEvent<SVGRectElement>
        ) => {
            const isHpaEnabled = hpaEnabled && !!hpaData.length;

            const { x } = localPoint(event) || { x: 0 };
            const x0 = dateScale.invert(x);

            const index = bisectDate(globalData, x0, 1);
            const d0 = globalData[index - 1];
            const d1 = globalData[index];
            let d = d0;

            if (d1 && getDate(d1)) {
                d =
                    x0.valueOf() - getDate(d0).valueOf() >
                        getDate(d1).valueOf() - x0.valueOf()
                        ? d1
                        : d0;
            }

            const hpaIndex = bisectDate(hpaData, x0, 1);
            // Get new index without min value to be sure that data exists for HPA
            const hpaIndex2 = bisectDate(hpaData, x0);

            if (!isHpaEnabled || hpaIndex !== hpaIndex2) {
                showTooltip({
                    tooltipData: {
                        data: d,
                        tooltipHpaData: undefined,
                        aggregatedData: getAggregatedDataTooltip(x0),
                    },
                    tooltipLeft: x || 0,
                    tooltipTop: valueScale(getValue(d)) || 0,
                });
                return;
            }

            const tooltipHpaData0 = hpaData[hpaIndex - 1];
            const tooltipHpaData1 = hpaData[hpaIndex];
            let tooltipHpaData = tooltipHpaData0;

            if (tooltipHpaData1 && getDate(tooltipHpaData1)) {
                tooltipHpaData =
                    x0.valueOf() - getDate(tooltipHpaData0).valueOf() >
                        getDate(tooltipHpaData1).valueOf() - x0.valueOf()
                        ? tooltipHpaData1
                        : tooltipHpaData0;
            }

            const container: SVGSVGElement = svgContainer.current;

            let point = container.createSVGPoint();
            // @ts-ignore
            point.x = (event as any)?.clientX || 0;
            // @ts-ignore
            point.y = (event as any)?.clientY || 0;
            point = point?.matrixTransform(container.getScreenCTM().inverse());

            showTooltip({
                tooltipData: {
                    data: d,
                    tooltipHpaData,
                    aggregatedData: getAggregatedDataTooltip(x0),
                },
                tooltipLeft: x || 0,
                tooltipTop: point.y || 0,
            });
        },
        [
            showTooltip,
            valueScale,
            dateScale,
            width,
            height,
            data,
            hpaData,
            svgContainer,
            hpaEnabled,
        ]
    );

    if (width == 0 || height == 0 || width < 10) {
        return null;
    }

    const hpaGraphTooltipGlyphPosition =
        (hpaEnabled &&
            tooltipData?.tooltipHpaData &&
            valueScale(getValue(tooltipData?.tooltipHpaData))) ||
        null;

    const dataGraphTooltipGlyphPosition =
        (tooltipData?.data && valueScale(getValue(tooltipData.data))) || 0;

    return (
        <div>
            <svg width={width} height={height} ref={svgContainer}>
                <rect
                    x={0}
                    y={0}
                    width={width}
                    height={height}
                    fill="url(#area-background-gradient)"
                    rx={14}
                />

                <LinearGradient
                    id="area-background-gradient"
                    from={ColorTheme.background}
                    to={ColorTheme.background2}
                />
                <LinearGradient
                    id="area-gradient"
                    from={ColorTheme.accentColor}
                    to={ColorTheme.accentColor}
                    toOpacity={0}
                />
                {Object.entries(AggregatedDataColors).map(([dataKey, color]) => (
                    <LinearGradient
                        key={dataKey}
                        id={`area-gradient-${dataKey}`}
                        from={color}
                        to={color}
                        toOpacity={0}
                    />
                ))}
                <GridRows
                    left={margin.left}
                    scale={valueScale}
                    width={innerWidth}
                    strokeDasharray="1,3"
                    stroke="white"
                    strokeOpacity={0.2}
                    pointerEvents="none"
                />
                <GridColumns
                    top={margin.top}
                    scale={dateScale}
                    height={innerHeight}
                    strokeDasharray="1,3"
                    stroke="white"
                    strokeOpacity={0.2}
                    pointerEvents="none"
                />
                <AreaClosed<NormalizedMetricsData>
                    data={data}
                    x={(d) => dateScale(getDate(d)) ?? 0}
                    y={(d) => valueScale(getValue(d)) ?? 0}
                    height={innerHeight}
                    yScale={valueScale}
                    strokeWidth={1}
                    stroke="url(#area-gradient)"
                    fill="url(#area-gradient)"
                    curve={curveMonotoneX}
                />
                {Object.entries(aggregatedData).map(([key, data]) => (
                    <LinePath<NormalizedMetricsData>
                        key={key}
                        data={data}
                        x={(d) => dateScale(getDate(d)) ?? 0}
                        y={(d) => valueScale(getValue(d)) ?? 0}
                        height={innerHeight}
                        strokeWidth={1}
                        stroke={AggregatedDataColors[key]}
                        // fill={`url(#area-gradient-${key})`}
                        curve={curveMonotoneX}
                    />
                ))}
                {isHpaEnabled && (
                    <LinePath<NormalizedMetricsData>
                        stroke="#ffffff"
                        strokeWidth={2}
                        data={hpaData}
                        x={(d) => dateScale(getDate(d)) ?? 0}
                        y={(d) => valueScale(getValue(d)) ?? 0}
                        strokeDasharray="6,4"
                        strokeOpacity={1}
                        pointerEvents="none"
                    />
                )}

                <AxisLeft
                    left={10}
                    scale={valueScale}
                    hideAxisLine={true}
                    numTicks={6}
                    hideTicks={true}
                    tickLabelProps={() => ({
                        fill: "white",
                        fontSize: 11,
                        textAnchor: "start",
                        fillOpacity: 0.4,
                        dy: 0,
                    })}
                />
                <AxisBottom
                    top={height - 20}
                    scale={dateScale}
                    tickFormat={formats[resolution]}
                    hideAxisLine={true}
                    hideTicks={true}
                    tickLabelProps={() => ({
                        fill: "white",
                        fontSize: 11,
                        textAnchor: "middle",
                        fillOpacity: 0.4,
                    })}
                />
                <Bar
                    x={margin.left}
                    y={margin.top}
                    width={innerWidth}
                    height={innerHeight}
                    fill="transparent"
                    rx={14}
                    onTouchStart={handleTooltip}
                    onTouchMove={handleTooltip}
                    onMouseMove={handleTooltip}
                    onMouseLeave={() => hideTooltip()}
                />
                {tooltipData && (
                    <g>
                        <Line
                            from={{ x: tooltipLeft, y: margin.top }}
                            to={{ x: tooltipLeft, y: innerHeight + margin.top }}
                            stroke={ColorTheme.accentColorDark}
                            strokeWidth={2}
                            pointerEvents="none"
                            strokeDasharray="5,2"
                        />
                        <circle
                            cx={tooltipLeft}
                            cy={dataGraphTooltipGlyphPosition + 1}
                            r={4}
                            fill="black"
                            fillOpacity={0.1}
                            stroke="black"
                            strokeOpacity={0.1}
                            strokeWidth={2}
                            pointerEvents="none"
                        />
                        {Object.entries(tooltipData.aggregatedData).map(([key, d]) => (
                            <circle
                                key={key}
                                cx={tooltipLeft}
                                cy={valueScale(getValue(d)) + 1}
                                r={4}
                                fill="black"
                                fillOpacity={0.1}
                                stroke="black"
                                strokeOpacity={0.1}
                                strokeWidth={2}
                                pointerEvents="none"
                            />
                        ))}
                        <circle
                            cx={tooltipLeft}
                            cy={dataGraphTooltipGlyphPosition}
                            r={4}
                            fill={ColorTheme.accentColorDark}
                            stroke="white"
                            strokeWidth={2}
                            pointerEvents="none"
                        />
                        {Object.entries(tooltipData.aggregatedData).map(([key, d]) => (
                            <circle
                                key={key}
                                cx={tooltipLeft}
                                cy={valueScale(getValue(d))}
                                r={4}
                                fill={ColorTheme.accentColorDark}
                                stroke="white"
                                strokeWidth={2}
                                pointerEvents="none"
                            />
                        ))}
                        {isHpaEnabled && hpaGraphTooltipGlyphPosition !== null && (
                            <>
                                <circle
                                    cx={tooltipLeft}
                                    cy={hpaGraphTooltipGlyphPosition + 1}
                                    r={4}
                                    fill="black"
                                    fillOpacity={0.1}
                                    stroke="black"
                                    strokeOpacity={0.1}
                                    strokeWidth={2}
                                    pointerEvents="none"
                                />
                                <circle
                                    cx={tooltipLeft}
                                    cy={hpaGraphTooltipGlyphPosition}
                                    r={4}
                                    fill={ColorTheme.accentColorDark}
                                    stroke="white"
                                    strokeWidth={2}
                                    pointerEvents="none"
                                />
                            </>
                        )}
                    </g>
                )}
            </svg>
            {tooltipData && (
                <div>
                    <TooltipWithBounds
                        key={Math.random()}
                        top={tooltipTop - 12}
                        left={tooltipLeft + 12}
                        style={{
                            ...defaultStyles,
                            background: "#26272f",
                            color: "#aaaabb",
                        }}
                    >
                        <TooltipDate>{formatDate(getDate(tooltipData.data))}</TooltipDate>
                        {isAggregated && (
                            <TooltipDataRow>
                                AVG. {dataKey}: {getValue(tooltipData.data)}
                            </TooltipDataRow>
                        ) || (
                            <TooltipDataRow>
                                Pod {dataKey}: {getValue(tooltipData.data)}
                            </TooltipDataRow>
                        )}
                        {Object.entries(tooltipData.aggregatedData).map(([key, value]) => (
                            <TooltipDataRow color={AggregatedDataColors[key]} key={key}>
                                {`${key.toUpperCase()}. ${dataKey}`}: {getValue(value)}
                            </TooltipDataRow>
                        ))}
                        {isHpaEnabled && hpaGraphTooltipGlyphPosition !== null && (
                            <div style={{ color: "#FFF" }}>
                                Autoscaling Threshold: {getValue(tooltipData.tooltipHpaData)}
                            </div>
                        )}
                    </TooltipWithBounds>
                </div>
            )}
        </div>
    );
};

export default AreaChart;

const TooltipDate = styled.div`
  text-align: center;
  margin-bottom: 8px;
`;

const TooltipDataRow = styled.div<{ color?: string }>`
  color: ${(props) => props.color ?? ColorTheme.accentColor};
  margin-bottom: 4px;
`;
